using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace LastFMPPC
{
	/// <summary>
	/// States of the LastFM client.
	/// </summary>
	public enum LastFMState
	{
		Disconnected,
		Connecting,
		Connected,
		Buffering,
		Playing,
		Disconnecting,
		Error
	}

	public delegate void LastFMStateCB(LastFMState state);
	public delegate void LastFMErrorCB(string message);
	public delegate void LastFMMetadataCB(Metadata metadata);

	/// <summary>
	/// The LastFM client.
	/// </summary>
	public class LastFM : IDisposable
	{
		private const string host = "ws.audioscrobbler.com";
		private const string port = "80";
		private const string version = "0.0.1";
		private const string platform = "windows";

		private string name = "";
		private string passwordMd5 = "";

		private string session = "";
		private string streamUrl = "";
		private string baseUrl = "";
		private string basePath = "";

		private LastFMState state = LastFMState.Disconnected;

		private const int streamingUnit = 4000;
		private const int ringbufferSlots = 8;
		private byte[] streamingBuffer = new byte[streamingUnit];
		private byte[] playingBuffer = new byte[streamingUnit];
		private Ringbuffer ringbuffer = new Ringbuffer(ringbufferSlots * streamingUnit);

		bool quit = false;
		private Thread streamingThread = null;
		private Thread playingThread = null;

		#region Constructor and Dispose

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="stateCB">Callback retrieving state change events.</param>
		/// <param name="errorCB">Callback retrieving error events.</param>
		public LastFM(LastFMStateCB stateCB, LastFMErrorCB errorCB, LastFMMetadataCB metadataCB)
		{
			if (stateCB != null) onState += stateCB;
			if (errorCB != null) onError += errorCB;
			if (metadataCB != null) onMetadata += metadataCB;

			streamingThread = new Thread(new ThreadStart(StreamingProc));
			playingThread = new Thread(new ThreadStart(PlayingProc));
		}

		private bool disposed = false;

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected void Dispose(bool disposing)
		{
			if (!this.disposed)
			{
				// quit and wait threads
				State = LastFMState.Disconnecting;
				quit = true;

				// a bit hack :)
				Thread.Sleep(3000);

				State = LastFMState.Disconnected;
			}
			disposed = true;
		}

		#endregion

		#region Event handlers and callbacks

		public LastFMStateCB onState = null;
		public LastFMErrorCB onError = null;
		public LastFMMetadataCB onMetadata = null;

		public LastFMState LastFMState
		{
			get { return state; }
		}

		private LastFMState State
		{
			set
			{
				state = value;
				if (onState != null) onState(state);
			}
		}

		private void Error(string message)
		{
			if (onError != null) onError(message);
		}

		private void MetadataHandler(Metadata metadata)
		{
			if (onMetadata != null) onMetadata(metadata);
		}

		#endregion

		/// <summary>
		/// Connects to the LastFM server.
		/// </summary>
		/// <param name="name">Username.</param>
		/// <param name="passwordMd5">MD5 hashed password.</param>
		public void Connect(string name, string passwordMd5)
		{
			if (state != LastFMState.Disconnected) return;

			this.name = name;
			this.passwordMd5 = passwordMd5;

			try
			{
				playingThread.Start();
				streamingThread.Start();
			}
			catch (Exception exp)
			{
				State = LastFMState.Disconnected;
				throw new LastException("LastFM.Connect: " + exp.Message);
			}
		}

		private void StreamingProc()
		{
			SocketConnection streamingSocket = null;

			try
			{
				// connecting.
				if (quit) return;
				State = LastFMState.Connecting;

				// LastFM handshake
				HttpClient hc = new HttpClient(host, port);
				hc.Req("/radio/handshake.php?version=" + version + "&platform=" + platform + "&username=" + name + "&passwordmd5=" + passwordMd5);
				Dictionary<string, string> ht = ParseResponse(hc.Response);
				if (!ht.ContainsKey("session"))
				{
					throw new LastException("LastFM.ConnectProc: Bad handshake!");
				}
				session = ht["session"];
				if (session == "FAILED")
				{
					throw new LastException("LastFM.ConnectProc: Invalid username or password!");
				}
				if (!ht.ContainsKey("stream_url") || !ht.ContainsKey("base_url") || !ht.ContainsKey("base_path"))
				{
					throw new LastException("LastFM.ConnectProc: No streaming url!");
				}

				// connected.
				if (quit) return;
				State = LastFMState.Connected;

				streamUrl = ht["stream_url"];
				baseUrl = ht["base_url"];
				basePath = ht["base_path"];

				// processing stream url
				string[] url = streamUrl.Split("/".ToCharArray());
				if (url.Length < 4)
				{
					throw new LastException("LastFM.ConnectProc: Invalid streaming url!");
				}

				string[] hostAndPort = url[2].Split(":".ToCharArray());
				int streamingPort = 80;
				if (hostAndPort.Length >= 2) streamingPort = int.Parse(hostAndPort[1]);

				if (quit) return;

				// connecting to the streaming server and processing its answer.
				streamingSocket = new SocketConnection(hostAndPort[0], streamingPort);
				streamingSocket.Connect();
				streamingSocket.Send("GET /" + url[3] + " HTTP/1.0\r\n");
				streamingSocket.Send("Host: " + hostAndPort[0] + "\r\n");
				streamingSocket.Send("\r\n");

				// retrieving and processing response
				List<string> streamingResponse = new List<string>();
				string line;
				do
				{
					line = streamingSocket.ReadLine();
					if (line.Length > 0) streamingResponse.Add(line);
				} while (line.Length > 0);

				// check if response is OK
				if (streamingResponse[0].IndexOf("OK") == -1)
				{
					throw new LastException("LastFM.ConnectProc: Bad streaming server response!");
				}

				// start streaming
				if (quit) return;
				State = LastFMState.Buffering;

				int readSize;
				int bufferOffset;
				int putSize;
				while (!quit)
				{
					readSize = streamingSocket.Read(streamingBuffer, streamingBuffer.Length, 0);

					// buffering streamed data
					for (bufferOffset = 0; bufferOffset < readSize && (!quit); bufferOffset += putSize)
					{
						putSize = ringbuffer.Put(streamingBuffer, bufferOffset, readSize - bufferOffset);
						if (putSize == 0) Thread.Sleep(10);
					}
				}
			}
			catch (LastException lexp)
			{
				Error(lexp.Text);
				State = LastFMState.Error;
			}
			catch (Exception exp)
			{
				Error("LastFM.ConnectProc: " + exp.Message);
				State = LastFMState.Error;
			}
			finally
			{
				if (streamingSocket != null) streamingSocket.Close();
			}
		}

		private void PlayingProc()
		{
			try
			{
				// init the mp3playa engine.
				Mp3Playa.Init();

				// start playing
				while (!quit)
				{
					if (state == LastFMState.Playing)
					{
						int getSize = ringbuffer.Get(playingBuffer);
						if (getSize > 0)
						{
							Mp3Playa.Enqueue(playingBuffer, getSize);
							if (!quit) State = LastFMState.Playing;
						}
						if (ringbuffer.Load < 10)
						{
							if (!quit) State = LastFMState.Buffering;
						}
					}
					else if (state == LastFMState.Buffering)
					{
						if (ringbuffer.Load > 95)
						{
							if (!quit) State = LastFMState.Playing;
						}
						else
						{
							if (!quit) State = LastFMState.Buffering;
							Thread.Sleep(10);
						}
					}
				}
			}
			catch (LastException lexp)
			{
				Error(lexp.Text);
				State = LastFMState.Error;
			}
			catch (Exception exp)
			{
				Error("LastFM.PlayingProc: " + exp.Message);
				State = LastFMState.Error;
			}
			finally
			{
				Mp3Playa.Close();
			}
		}

		/// <summary>
		/// Perform a command.
		/// </summary>
		/// <param name="cmd">Command string</param>
		public void Command(string cmd)
		{
			try
			{
				if (state == LastFMState.Connected || state == LastFMState.Playing || state == LastFMState.Buffering)
				{
					ThreadPool.QueueUserWorkItem(new WaitCallback(CommandProc), cmd);
				}
			}
			catch (LastException lexp)
			{
				throw lexp;
			}
			catch (Exception exp)
			{
				throw new LastException("LastFM.Command: " + exp.Message);
			}
		}

		private void CommandProc(object state)
		{
			try
			{
				string cmd = state as string;
				if (cmd != null)
				{
					HttpClient hc = new HttpClient(baseUrl, "80");
					hc.Req(basePath + "/control.php?command=" + cmd + "&session=" + session);
				}
			}
			catch (LastException lexp)
			{
				Error(lexp.Text);
			}
			catch (Exception exp)
			{
				Error("CommandProc: " + exp.Message);
			}
		}

		/// <summary>
		/// Changes the station.
		/// </summary>
		/// <param name="url">Url.</param>
		/// <returns>Hashtable containing the response.</returns>
		public Dictionary<string, string> ChangeStation(string url)
		{
			try
			{
				if (state == LastFMState.Connected || state == LastFMState.Playing)
				{
					HttpClient hc = new HttpClient(baseUrl, "80");
					hc.Req(basePath + "/adjust.php?session=" + session + "&url=" + url);
					return ParseResponse(hc.Response);
				}
				return null;
			}
			catch (LastException lexp)
			{
				throw lexp;
			}
			catch (Exception exp)
			{
				throw new LastException("LastFM.ChangeStation: " + exp.Message);
			}
		}

		/// <summary>
		/// Gets the current metadata.
		/// </summary>
		/// <returns>The metadata instance.</returns>
		public void GetMetadata()
		{
			try
			{
				if (state == LastFMState.Connected || state == LastFMState.Playing)
				{
					ThreadPool.QueueUserWorkItem(new WaitCallback(GetMetadataProc));
				}
			}
			catch (LastException lexp)
			{
				throw lexp;
			}
			catch (Exception exp)
			{
				throw new LastException("LastFM.GetMetadata: " + exp.Message);
			}
		}

		private void GetMetadataProc(object state)
		{
			try
			{
				HttpClient hc = new HttpClient(baseUrl, "80");
				hc.Req(basePath + "/np.php?session=" + session);
				Dictionary<string, string> dic = ParseResponse(hc.Response);
				if (dic.ContainsKey("streaming"))
				{
					if (dic["streaming"] == "false" ||
						(dic["streaming"] == "true" && dic.ContainsKey("artist") && dic.ContainsKey("track") && dic.ContainsKey("trackduration")))
					{
						Metadata metadata = new Metadata();
						metadata.Artist = dic.ContainsKey("artist") ? dic["artist"] : "";
						metadata.Track = dic.ContainsKey("track") ? dic["track"] : "";
						metadata.TrackDuration = dic.ContainsKey("trackduration") ? dic["trackduration"] : "";
						metadata.Album = dic.ContainsKey("album") ? dic["album"] : "";
						metadata.AlbumUrl = dic.ContainsKey("album") ? dic["album_url"] : "";

						MetadataHandler(metadata);
					}
				}
			}
			catch (LastException lexp)
			{
				Error(lexp.Text);
			}
			catch (Exception exp)
			{
				Error("GetMetadataProc: " + exp.Message);
			}
		}

		/// <summary>
		/// Returns the state in text format.
		/// </summary>
		/// <param name="state">State.</param>
		/// <returns>State in string.</returns>
		public string StateToString(LastFMState state)
		{
			switch (state)
			{
				case LastFMState.Disconnected:
					return "Disconnected.";
				case LastFMState.Connecting:
					return "Connecting...";
				case LastFMState.Connected:
					return "Connected.";
				case LastFMState.Buffering:
					return string.Format("Buffering: {0}%...", ringbuffer.Load);
				case LastFMState.Playing:
					return string.Format("Playing. Buffer load: {0}%", ringbuffer.Load);
				case LastFMState.Disconnecting:
					return "Disconnecting...";
				case LastFMState.Error:
					return "Error!";
			}
			return "Unknown state.";
		}

		/// <summary>
		/// Sets the volulem.
		/// </summary>
		/// <param name="volume">The new volume.</param>
		public void SetVolume(int volume)
		{
			try
			{
				if (state == LastFMState.Connected || state == LastFMState.Playing)
				{
					Mp3Playa.Volume(volume);
				}
			}
			catch (LastException lexp)
			{
				throw lexp;
			}
			catch (Exception exp)
			{
				throw new LastException("LastFM.SetVolume: " + exp.Message);
			}
		}

		/// <summary>
		/// Parses the http response list.
		/// </summary>
		/// <param name="list">List containing strings.</param>
		/// <returns>Dictionary containing the key/value pairs.</returns>
		private Dictionary<string, string> ParseResponse(List<string> list)
		{
			try
			{
				Dictionary<string, string> res = new Dictionary<string, string>();
				foreach (string s in list)
				{
					string[] temp = s.Split("=".ToCharArray());
					if (temp.Length >= 2 && !res.ContainsKey(temp[0]))
					{
						if (temp.Length > 2) temp[1] += "=" + temp[2];
						res.Add(temp[0], temp[1]);
					}
				}
				return res;
			}
			catch (Exception exp)
			{
				throw new LastException("LastFM.ParseResponse: " + exp.Message);
			}
		}
	}
}
